There are a number of different ways to read and
write files in Visual Basic, and which you choose depends on what you
are trying to do, as described in Table 1.
Table 1. File-access techniques in Excel Visual Basic
Technique | Use to |
---|
Intrinsic functions | Read or write simple datafiles |
FileSystemObject | Create files, folders, and control file attributes |
Workbooks, Workbook objects | Create, open, and save Excel workbook files; import datafiles into workbooks |
XMLMap object | Import or export XML datafiles from a workbook |
In short, you shouldn't
assume the Visual Basic intrinsic functions are the best way to read and
write files in all situations. Actually, I prefer the FileSystemObject for most general file-access tasks, but it's important to be thorough, so I'll cover the intrinsic file-access functions here (Table 2).
Table 2. Visual Basic's intrinsic file-access functions
Category | Function | Use to |
---|
Access | Close | Close an open file |
| FileCopy | Copy a file |
| FreeFile | Get a file number for Open |
| Lock...Unlock | Prevent others from accessing all or part of a file |
| LOF | Get the length of an open file in bytes |
| Open | Open a file |
| Reset | Close all open files |
Attributes | FileAttr | Get the attributes of an open file |
| FileDateTime | Get the date that a file was created or changed |
| FileLen | Get the length of a file in bytes before opening it |
| GetAttr | Get the attributes of a file, folder, or volume label |
| SetAttr | Change the attributes of a file, folder, or volume label |
Drives | ChDir | Set the current folder |
| ChDrive | Set the current drive |
| CurDir | Get the current folder |
| MkDir | Create a new folder |
| RmDir | Delete an empty folder |
Manage | Dir | List files in a folder |
| Kill | Delete a file |
| Name | Rename a file |
Read | Get | Read data from an open binary or random-access file |
| EOF | Test you have reached the end of the file |
| Input # | Read records from an open sequential file |
| Line Input #
| Read a line from an open sequential file |
| Loc | Return the current position within a file |
| Seek | Get or set the current position within a file |
Write | Print # | Write a line to an open sequential file |
| Put | Write data to an open binary or random-access file |
| Spc | Insert blank spaces in a sequential file |
| Tab | Insert tab characters in a sequential file |
| Width # | Set the width of a sequential file |
| Write # | Write records to a sequential file |
The functions in Table 2 reflect the fact that there are three different types of file access in Visual Basic:
Sequential access
Reads files one line at a time
Random access
Reads files as a collection of fixed-length records
Binary access
Reads files as an arbitrary number of bytes
All of these types of access follow the same pattern, which is based on a very old programming concept called file handles
:
Use FreeFile to get a number that is available for use as a file handle.
Open the file using that number and the chosen file-access method.
Read data from the file using Input # (sequential access) or Get (random or binary access), or write data using Print #, Write # (sequential), or Put (random or binary).
The modern approach, such as that used by the FileSystemObject, is to use object references rather than numeric file handles. |
|
Of the three types of file
access, binary is the most useful in today's world because it allows you
to read an entire file into a variable with a single statement. It is
by far the fastest way to get the contents of a file. The following QuickRead function opens and reads a file and returns the data it contains as a string variable:
' Reads a file into a string.
Function QuickRead(fname As String) As String
Dim i As Integer, res As String, l As Long
' Get a free file handle.
i = FreeFile
' Get the length of the file
l = FileLen(fname)
' Create a string to contain the data.
res = Space(l)
' Open the file.
Open fname For Binary Access Read As #i
' Read the whole file into res.
Get #i, , res
' Close the file
Close i
' Return the string.
QuickRead = res
End Function
How big of a file can you
read this way? Pretty big! I had no problem loading an 8.4 MB art file
using this technique. String variables can be very large in Visual Basic. Similarly, you can write files very quickly with binary access. The following QuickWrite function saves a string as a file and returns True if it succeeded:
' Writes data to a file.
Function QuickWrite(data As String, fname As String, _
Optional overwrite As Boolean = False) As Boolean
Dim i As Integer, l As Long
' If file exists and overwrite is True, then
If Dir(fname) <> "" Then
If overwrite Then
' delete the file.
Kill fname
Else
' else, return False and exit.
QuickWrite = False
Exit Function
End If
End If
' Get a free file handle.
i = FreeFile
' Get the length of the file
l = Len(data)
' Open the file.
Open fname For Binary Access Write As #i Len = l
' Write the string to the file.
Put #i, , data
' Close the file
Close i
' Return True.
QuickWrite = True
End Function
This approach was first
pointed out to me by Mark Chase, senior developer on Basic at Microsoft.
He deserves credit for clear thinking and also for being a darn nice
guy. You can test that these functions work by running the following
code from the sample workbook:
Sub DemoQuickReadWrite( )
Dim pth As String, data As String
' Get the folder that this workbook is in.
pth = ThisWorkbook.Path
' Read the ReadMe.txt file.
data = QuickRead(pth & "\in.txt")
' Display the file
Debug.Print data
' Change the file.
data = Replace(data, "text", "data")
' Save the file.
Debug.Print QuickWrite(data, pth & "\out.txt", True)
End Sub
1. Sequential Access
Sequential access reads and writes files one line at a time. In the past, sequential access
was often used to write reports or other data to human-readable files. For example, the following WriteArray function writes a two-dimensional array to disk as a comma-delimited file using sequential access:
' Writes a two-dimensional array to a comma-delimited file.
' (Use to create CSV file out of a selected range.)
Function WriteArray(arr As Variant, fname As String, _
Optional overwrite As Boolean = False) As Boolean
Dim lb1 As Long, lb2 As Long, ub1 As Long, ub2 As Long
Dim i As Integer, rows As Long, cols As Long, rec As String
' If arr isn't an array, return False and exit.
If Not IsArray(arr) Then WriteArray = False: Exit Function
' Get bounds for For loops.
lb1 = LBound(arr, 1)
lb2 = LBound(arr, 2)
ub1 = UBound(arr, 1)
ub2 = UBound(arr, 2)
' If file exists and overwrite is True, then
If Dir(fname) <> "" Then
If overwrite Then
' delete the file.
Kill fname
Else
' else, return False and exit.
WriteArray = False
Exit Function
End If
End If
' Get a free file handle.
i = FreeFile
' Open the file.
Open fname For Append As #i
' For each row in the array.
For rows = lb1 To ub1
' For each column in the array.
For cols = lb2 To ub2
rec = rec & arr(rows, cols) & ", "
Next
' Remove the last ", " from rec.
rec = Left(rec, Len(rec) - 2)
' Write rec to the file.
Print #i, rec
' Clear rec
rec = ""
Next
' Close the file
Close i
' Return True.
WriteArray = True
End Function
That looks complicated, but the actual file-access code (in bold)
is really very simple and follows the pattern described previously: get
a file handle, open the file, read or write to the file, close the
file. Sequential access is suited to this task since you are building
the string data one line at a time as you loop over the rows in the
array.
Perhaps a better approach to
this task would be to build a string from the array in one procedure
and then save that string using the QuickWrite function. That approach would isolate file access in one place (QuickWrite) instead of integrating it into the task of converting the array into a string. The following code shows that alternate approach:
' Better approach -- don't integrate file access within
' array conversion task.
Function TableToCSV(arr As Variant) As String
Dim lb1 As Long, lb2 As Long, ub1 As Long, ub2 As Long
Dim rows As Long, cols As Long, rec As String
' If arr is not an array, return "" and exit.
If Not IsArray(arr) Then TableToCSV = "": Exit Function
' Get bounds for For loops.
lb1 = LBound(arr, 1)
lb2 = LBound(arr, 2)
ub1 = UBound(arr, 1)
ub2 = UBound(arr, 2)
For rows = lb1 To ub1
For cols = lb2 To ub2
rec = rec & arr(rows, cols) & ", "
Next
' Remove last ", " and add carriage return/line feed.
rec = Left(rec, Len(rec) - 2) & vbCrLf
Next
TableToCSV = rec
End Function
Using TableToCSV instead of WriteArray involves the extra step of calling QuickWrite, but the logic is still very clear:
Sub DemoTableToCSV( )
Dim arr As Variant, data As String, pth As String
pth = ThisWorkbook.Path
' Get cells from the active worksheet.
arr = ActiveSheet.UsedRange.Value
' If the range contains cells.
If IsArray(arr) Then
' Convert array to CSV.
data = TableToCSV(arr)
If data <> "" Then
' Save the result
QuickWrite data, pth & "\selection.csv", True
' Display the result
Debug.Print data
End If
End If
End Sub
2. Random Access
Random-access files are read or written one record at a time. In this case, record
usually means a fixed-size data structure identified by a user-defined
type. Because Visual Basic knows the length of each record, you can jump
to any record in the file using the Seek statement (that's what makes the access random).
In order to use random access
, you must first define the structure of your record with a Type
statement. You then declare a variable with that type and use it to
read and/or write records to the file. I'm not going to show you how to
do all that, because XML files and databases both provide a much better
approach for storing and retrieving structured data.
Why is random access not such a great approach? A few reasons:
The records are
fixed-length by definition, which means names, addresses, and other
variable-length data must be stored in fixed-length strings. You have to
correctly guess the maximum size of those items during design.
Changes
to your data structure, such as adding a field, means you have to
convert all of your existing datafiles. You have to write code to open,
convert, and save files using the new structure. (In programming circles
this is called tying your data structure to your implementation, and
it's a bad thing.)
You're programming in Excel! You already have better tools for doing these types of tasks.
3. Common Tasks
In addition to reading
and writing files, you also often need to manage the files on a
computer. The most common tasks are listed in Table 3.
Table 3. Common tasks for Visual Basic's intrinsic file functions
Task | Function | Comments |
---|
Check if file exists | Dir | Also used to list files in a folder. |
Delete a file | Kill | Deletes a file if it is not locked or read-only. |
Get the current folder | CurDir | Excel may change the current folder when a workbook is saved or opened by the user. |
Change current folder | ChDir | You can use characters like .. to move up one folder. |
Change current drive | ChDrive | Only the first letter from the argument is used. |
Create a folder | MkDir | May include path specifiers like . (current folder) or .. (up one folder). Does not change the current folder. |
Delete a folder | RmDir | Folder must be empty before it can be deleted. |
Get/change file attributes | FileAttr | File attributes include hidden, read-only, archive. |
Make a backup copy | FileCopy | Copies an existing file to a new filename. |
Rename a file | Name | Changes a filename. |
In general, it is not a good idea to get the current folder (CurDir) or change the current folder (ChDir)
from Visual Basic when working with Excel because saving or opening a
file from the Excel user interface may subsequently change the current
folder. It is a better practice to use the path properties provided by
Excel objects when working with folders in Excel.
For example, the following code displays the paths available for various Excel objects:
Sub ShowPaths( )
Dim wbPth As String, appPth As String, stPth As String, _
altPth As String, tpPth As String, adPth As String
wbPth = ThisWorkbook.Path
appPth = Application.Path
stPth = Application.StartupPath
altPth = Application.AltStartupPath
tpPth = Application.TemplatesPath
adPth = Application.AddIns(1).Path
Debug.Print "Workbook path:", wbPth
Debug.Print "Application path:", appPth
Debug.Print "Startup path: ", stPth
Debug.Print "Alt startup path:", altPth
Debug.Print "Template path:", tpPth
Debug.Print "Add-in path: ", adPth
End Sub
I often use ThisWorkbook.Path
within my samples to get or save files associated with the current
workbook. That strategy keeps all of the related files in the same
folder, so it is easier to copy the samples to a new location or to
install them on your computer. Alternately, you may choose to create a
fixed folder location for use in your code such as shown here:
' A fixed path.
Const SAMPLEPTH = "\Excel\Samples"
' Run once to create folder.
Sub CreateSamplesFolder( )
' Create the SAMPLEPTH folder
On Error Resume Next
MkDir "\Excel"
MkDir "\Excel\Samples"
If Err Then _
MsgBox ("Couldn't create folder. It may already exist.")
End Sub
Using a fixed location
for your files poses the problem illustrated by the preceding exception
handling: the folder may already exist! That's another reason to use the
ThisWorkbook.Path approach.
You can use the Dir
function to check whether a file exists in a folder or to get a list of
all of the files in a folder. When getting a list of files, Dir acts a little strangely. The first time you call it, specify the folder you want to search; then call Dir without an argument to get the next file in the folder, as shown here:
Function GetFiles(filepath As String) As Variant
Dim arr( ) As String, fname As String, count As Integer
' Get the first file.
fname = Dir(filepath & "\*")
Do Until fname = ""
count = count + 1
ReDim Preserve arr(count)
arr(count - 1) = fname
' Get next file.
fname = Dir( )
Loop
' Return the array
GetFiles = arr
End Function
Dir does not
order the files it returns alphabetically, so you may need to sort the
list before displaying it. For example, the following code uses the GetFiles function to list the files in the current workbook's folder:
Sub DemoGetFiles( )
Dim flist As Variant, str As String
flist = GetFiles(ThisWorkbook.Path)
' Sort the file list
Text.SortArray (flist)
str = Join(flist, vbCrLf)
Debug.Print str
End Sub
The FileSystemObject provides more extensive methods for working with files, folders, and drives.